feat(workspace-team): team workspaces, shared resources, workspace-scoped billing, and the #5517 redesign - #6142
Draft
lefarcen wants to merge 629 commits into
Draft
feat(workspace-team): team workspaces, shared resources, workspace-scoped billing, and the #5517 redesign#6142lefarcen wants to merge 629 commits into
lefarcen wants to merge 629 commits into
Conversation
Both logout entry points — the nav-rail account menu's 退出登录 item and the AMR account pill's Sign out button (Settings, chat error cards, balance dialog) — used to clear the vela auth on a single click, so a stray click signed the user out instantly. They now arm a shared SignOutConfirmDialog built from the existing @open-design/components Dialog primitives in the standard modal-confirm alertdialog shape. Cancel/Escape leaves the session untouched; only the dialog's confirm action runs the real logout chain (velaLogout + the workspace-surface refresh nudges). New signOut.confirm* i18n keys are defined in types.ts and translated in all 19 locale files.
…ecvqholN2wcep) The home 示例提示词 card for 交易分析仪表板模板 rendered a gray broken frame instead of the template preview. Root cause: the daemon's sandboxed preview route serves example.html with `default-src 'none'` CSP (no frame-src), so a nested iframe can never load; the sanctioned pattern is the iframe-only shell, which iframeOnlyHtmlShellTarget unwraps so the inner template is served directly. This template's example.html wrapped its iframe with a header strip, defeating the unwrap — the CSP-blocked frame then baked into the CI gallery poster as a broken gray image. - Rewrite example.html (both the plugins/_official copy and the design-templates mirror) as a bare iframe-only shell, matching the proven flowai-live-dashboard-template shape. The served preview now unwraps to the self-contained assets/template.html. - Drop the stale broken bake entry from data/plugin-previews/manifest.json so clients fall back to the (now working) live HTML preview immediately, and the next CI bake sweep re-renders a fresh poster/clip. - Add an e2e repo-consistency guard: any bundled example.html embedding a sibling .html through an iframe must be a pure shell (fails on the old wrapper, passes now), so this class of broken preview can't return.
…recvqg3CzK8GcT) In the project chat composer footer the model provider mark sits between the session-mode pill and the run/send capsule. Its box gaps were equal (4px row gap + 4px trigger padding each side), but the visible spacing was not: the pill's × glyph keeps ~17px of ink distance from the mark (clear-button slack + pill trailing padding), while the solid send capsule's edge sat only ~9px away — so the icon read as glued to the run button (the two red boxes in the Feishu screenshot). The trigger never paints its hitbox in this context (hover stays transparent), so ink distance is the only spacing the eye can see. Add margin-inline-end: 8px to the composer-row trigger so the right ink gap matches the left one (17px on both sides). Scoped to .app .composer-row and .chat-composer-fixed-layer .composer-row; the drawer tabs-bar and home-hero switcher chips are untouched. Verified by rendering the exact composer markup against the real stylesheets (tokens/shell/chat/routines/plus-menu) in headless Chromium: before = 17px/9px, after = 17px/17px, matching the pixel measurements taken from the reporter's screenshot.
…n never-materialized team projects (recvqmnuxxKHaI) A member daemon's team catalog routinely lists projects this member has never opened or pulled. Those projects have no local `projects` row, and `workspace_projects.project_id` is a FOREIGN KEY into `projects(id)`, so the reconciler's bind fallback (`ensureWorkspaceProject` INSERT) threw SQLITE_CONSTRAINT_FOREIGNKEY for every such project on every pass — hub push and ~15s poller alike — accumulating 4700+ error lines across restarts on the live member instance. Reconciliation corrects the bindings of projects this daemon knows; it must never materialize one. Materialization is owned by the open/pull path (ensureSharedProjectPlaceholder / registerPulledProject), and the team list already displays never-materialized projects straight from the remote catalog. The reconciler now consults a new required `hasLocalProject` dep and skips remote entries with neither a local binding nor a local `projects` row — the same rebind-only stance its request-scoped sibling `reconcileLocalRowWithRemoteTeamAccess` has always taken. The demote direction is unaffected: demotes only ever come from existing local bindings, which the FK guarantees are materialized.
… modal
A personal-workspace user with a $0 balance who clicked 「升级套餐」 on the
AMR balance gate landed on the TEAM dashboard's billing=checkout deep link,
which opens the Upgrade-Personal-workspace-to-Team dialog in an error state
("Team plan unavailable" / 3-seat minimum) — recvpYEiH019cD, reproduced live
with a real personal account during the failed acceptance round.
Root cause: every upgrade entry picked its branch on "does the context carry
a workspaceSettingsUrl", falling back to the personal wallet deep link only
when it was absent. B returns a settings URL for personal workspaces too
(mapVelaWorkspaceContext resolves it for every workspace type, and B's
console genuinely has a settings page for them), so the fallback never fired
and every workspace — personal included — took the team branch.
Fix: the axis is the workspace TYPE, not URL presence. A new shared
`workspaceUpgradeUrl` helper (EntryNavRail.tsx, next to `teamConsoleUrl`)
makes the one decision every upgrade affordance shares:
- personal → `wallet?view=plans` (B's personal pricing modal; verified
live to auto-open for the same third-account session, and vela's own
wallet route gates the modal purely on `view=plans` for a personal
workspace context)
- team, never subscribed → `dashboard?billing=checkout` (unchanged from
cb48e06)
- team, already subscribed → `dashboard?billing=plan` (unchanged from
cb48e06)
All five upgrade entries route through the helper instead of scattering the
condition: EntryNavRail's credits chip and seat-gate invite dialog,
AmrBalanceDialog's balance-gate CTA, RecentProjectsStrip's invite dialog,
and SettingsDialog's two AMR-card upgrade buttons (via its shared
amrUpgradeUrl closure). Dialog CTAs keep a profile-keyed fallback so they
still link somewhere when no console URL is known (signed out / context not
landed); list-surface entries hide the affordance instead.
teamConsoleUrl grows a 'plans' section (wallet + view=plans, keeping the
?workspaceId pin). For a TEAM workspace B redirects that exact URL into
dashboard?billing=checkout itself, so even a misrouted team session degrades
to the first-checkout dialog rather than a dead page.
Tests: the AmrBalanceDialog component suite grows the personal-workspace
case (red on the previous code: it produced /console/dashboard with
billing=checkout) alongside the existing never-subscribed / already-
subscribed team cases; team-console-url.test.ts covers workspaceUpgradeUrl's
three states plus the null/fallback contract; the EntryNavRail seat-gate
test's personal fixture now asserts the wallet pricing modal instead of the
team checkout link it used to encode.
Verified against the real feature-test runtime with the third acceptance
account (3101794733@qq.com, personal workspace, $0 balance): the balance
gate's 「升级套餐」 now opens
wallet?workspaceId=...&view=plans (no billing param), the URL family
confirmed live to auto-open B's personal pricing dialog.
…content-changed (recvqmKQRiIlYf) When a teammate publishes a new version of a shared project, the hub already pushes 'project-content-changed' over the collab SSE channel -- but the daemon only forwarded a thin nudge to an OPEN project view, so freshness still hinged on the member web's ~5s /collab/status polling and a closed tab never synced at all. The daemon now owns discovery: on 'project-content-changed' it runs the SAME pull flow POST /api/projects/:id/collab/pull runs (revocation gate, owner-routed hub pull, register-on-pull, then the existing file-changed and project-metadata-changed signals -- zero web changes needed), via a handle registerCollabSyncRoutes returns. Guards, all fail-closed: - only projects locally bound as team projects in workspace_projects are pulled; a share the member never materialized costs nothing; - NEVER pulls a project this daemon's member owns -- the owner's local copy is the single writer (same rule as useProjectCollab's web-side auto-pull gate), and the owner daemon receives its own publish echo; - event workspace, local binding workspace, and active identity must agree, and only an active team membership pulls; - a per-project version cursor dedupes repeated/out-of-order events, and racing pulls (proactive vs the member web's poll-triggered POST) coalesce onto one in-flight materialization in collab-sync; - failures degrade silently: the web polling fallback stays untouched. Red-first specs: the three collab-sync handle tests fail on the branch baseline (registerCollabSyncRoutes returned void); the guard matrix for the new consumer lives in tests/collab/proactive-content-pull.test.ts. Full daemon suite: 29 failures on this branch == the same 29 on the clean baseline (langfuse-bridge/chat-route/etc., pre-existing).
Harden Workspace collaboration authority, lifecycle cleanup, and browser coverage.\n\nRetry an expired authorized Team-project pull exactly once with a fresh receipt while preserving fail-closed scope, version, cleanup, and final freshness checks. Align the fake Vela receipt lifetime with the real two-second contract and use scalable E2E timeouts.
Prevent stale Team-plugin listings, materializations, and delayed positive hub reads from reactivating a resource after retraction. Preserve independent Personal plugins with the same manifest ID and keep Team members able to share their own resources. Add deterministic regression coverage for cache invalidation, binding lifecycle, and the superseded-positive activation race.
Reject invalid wallet balances without overwriting valid cache state, validate normalized proxy paths and Workspace scope, strip hop-by-hop headers, and tear down upstream requests on disconnect. Keep team Workspace scope fail-closed when Connection nominates the protected header.
Preserve a verified explicit Workspace context when legacy directory rows omit the display-only workspaceName field. Keep exact-row name backfill when a non-empty name is present, without inferring authority from active or default Workspace state.
…sign-files-image-skeleton-20260803
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Use case.
feat/workspace-teamis the long-running integration branch for the Workspace Team programme (OPEND-444): shared team workspaces, project/design-system/plugin sharing, workspace-scoped billing, the #5517 visual redesign, and the AMR/vela collaboration transport. It has been developed and dogfooded for weeks behind a build-time gate; this PR opens the trunk-merge conversation and gives us a single place to run full CI againstmain.Pain being addressed. The branch is 497 commits ahead. Every extra day of divergence makes the merge more expensive (this PR already carries two
mainsyncs and 42 hand-resolved conflicts), and trunk currently has no CI signal for any of this work. Opening the PR now buys us that signal and a shared reconciliation checklist.What users will see
Nothing changes for users on the stable channel when this merges. Team-workspace behaviour is gated at build time:
apps/packaged/src/sidecars.tsonly enables the collab/resource/team-projects transports and the vela web origin whenamrProfile === "feature-test". Prod builds keep the current single-user experience until we flip that gate in a separate, deliberate change (sequenced after the vela backend ships — see the GA plan referenced below).What is in the branch, and what users will see once the gate is flipped:
Surface area
odworkspace/collab surfaces;OD_WORKSPACE_CONTEXT_SOURCE,OD_COLLAB_TRANSPORT,OD_RESOURCE_TRANSPORT,OD_TEAM_PROJECTS_TRANSPORT,OD_VELA_WEB_URL;OPEN_DESIGN_AMR_PROFILE=feature-test/api/workspaces/*,/api/workspace/context, collab sync/pull/status/presence/comments endpoints, new SSE event types, new error codes (e.g.TEAM_PROJECT_OWNER_CONFLICT) inpackages/contractsapps/web/src/i18n/types.ts@powerformer/vela-cliintools/packworkspace_projects, workspace-scoped resource tables); all behaviour behind the build-time profile gate for prod buildsScreenshots
Dogfood acceptance screenshots live in the Feishu tracker (base
HTHrbGc4xaJwjss04SKc0ePSnFh, tabletblTkanb8ddmWu9z), which is the authoritative status source for this programme; per-fix evidence is attached there record by record. Happy to inline a representative set here on request.Bug fix verification
This is an integration PR, not a single bug fix. Every bug fix on the branch followed the repo's red-spec rule (red on pre-fix code, green on the fix) — recent examples:
f62271ae9team members can comment on shared projects — red:apps/daemon/tests/project-comment-workspace-gate.test.ts(403 → 200)299802502reconciliation judged by catalog membership — red:apps/daemon/tests/collab/workspace-projects-reconcile-membership.test.tsbd249cc9afresh-install wipe guard — red:apps/daemon/tests/collab-fresh-install-placeholder-guard.test.ts358145514tab snapshots never adopted across identity scopes — red:apps/web/tests/components/WorkspaceTabsBar.test.tsx3aaa5dddcsettled content replaces the frozen Comment-mode canvas — red:apps/web/tests/components/FileViewer.board-mode-content-update.test.tsxcbcd8c4f0template preview portal — red:apps/web/tests/components/template-modal-mapping.test.tsxValidation
Run on the merge result of the two
mainsyncs contained here:pnpm guard— greenpnpm typecheck(all workspaces incl.e2e) — green, 0 errorspnpm --filter @open-design/daemon test— full suite; merge-introduced failures fixed, remaining 16 are pre-existing on the branch baseline (verified by re-running on a cleanfeatworktree)pnpm --filter @open-design/web test— 5390 passing; 6 failures reproduce identically on the baselineBlockers before merge
mainUI work intentionally dropped during the syncs. Per an explicit product ruling ("the interface follows the feature branch — it has been through many rounds of acceptance"), thesemainchanges were not carried into the branch and would be silently reverted by a naive merge:#5129workspace tab width,#4468home-hero contrast token (+ its test)Each needs a product decision: re-land on top of the branch, or accept the revert.
e2e/uihas not been run against the merged tree (typecheck/guard only). Aui_p0run should gate the merge.